package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.gulimall.product.entity.AttrEntity;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catalog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;


@Service("categoryService")
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;
    /**
     * 本地缓存
     */
    private Map<String, Object> cache = new HashMap<>();
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 首先查出所有的分类数据
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(null);
        // 过滤出所有的一级分类数据
        List<CategoryEntity> level = categoryEntities.stream().filter(x -> 0 == x.getParentCid())
                .map((menu) -> {
                    // 封装子菜单
                    menu.setChildren(getChildren(menu, categoryEntities));
                    return menu;
                }).sorted(Comparator.comparingInt(menu -> (Optional.ofNullable(menu.getSort()).orElse(0))
                )).collect(Collectors.toList());
        return level;
    }

    /**
     * @param root 当前菜单
     * @param all  所有数据
     * @return
     */
    private static List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        /**
         * 如果当前菜单的父类id 等于id 就是子菜单
         */
        List<CategoryEntity> children = all.stream().
                filter(menu -> menu.getParentCid().equals(root.getCatId())
                ).map(categoryEntity -> {
                    categoryEntity.setChildren(getChildren(categoryEntity, all));
                    return categoryEntity;
                }).sorted(Comparator.comparingInt(menu -> (Optional.ofNullable(menu.getSort()).orElse(0))))
                .collect(Collectors.toList());
        return children;
    }

    /**
     * 自定义删除
     *
     * @param asList
     */
    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO 检查当前菜单是否被其他菜单所引用
        baseMapper.deleteBatchIds(asList);
    }

    /**
     * 分类列表  [2,43,225]
     *
     * @param catelogId
     * @return
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> path = new ArrayList<>();
        // 查询出完整路径
        List<Long> parentPath = findParentPath(catelogId, path);
        // 逆序
        Collections.reverse(parentPath);
        // 转为数组
        return parentPath.toArray(new Long[0]);
    }

    /**
     * 级联更新所有 关联的数据
     *
     * @param category
     * @CacheEvict:失效模式 1、同时进行多种缓存操作 @Caching
     * 2、删除分区里面所有的数据 allEntries=ture
     * 3、存储同一类型的数据,都可以指定成同一个分区 分区名默认为缓存前缀
     */
    @Caching(evict = {
            @CacheEvict(value = "category", key = "getCatalogJson"),
            @CacheEvict(value = "category", key = "getFirstCategories")
    })
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        // 判断名字是否为空
        if (StringUtils.hasText(category.getName())) {
            // 更新冗余字段
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        }
    }

    /**
     * 1、每一个需要缓存的数据都需要指定要放到哪个名字的缓存[缓存的分区(按照业务类型来分)]
     * 2、代表当前的结果需要缓存，如果缓存中有，不需要调用方法。如果缓存中没有，会调用方法将返回结果放入缓存中。
     * 3、默认行为:
     * 1)、如果缓存中有，方法不用调用
     * 2）、key默认自动生成,缓存的名字::SimpleKey [](自主生成的key值)
     * 3)、缓存的value值。默认使用jdk序列化机制，将序列化后的数据存放到redis
     * 4)、默认ttl时间-1；
     * 4、自定义:
     * 1)、指定生成的缓存使用的key，key属性指定 接受一个spl
     * 2)、指定缓存的数据存活时间,配置文件修改ttl
     * 3)、将数据保存为json格式
     * 5、原理:
     * CacheAutoConfiguration->RedisAutoConfiguration->
     * 自动配置了RedisCacheManager->初始化所有的缓存->每个缓存决定使用什么配置
     * ->如果redisCacheConfiguration有就用已有的,没有就用默认配置
     * ->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可
     * ->就会应用到当前RedisCacheManager管理的所有缓存中。
     * CacheAutoConfiguration===>RedisCacheConfiguration==>RedisCacheManager
     * 6、Spring-Cache的不足:
     *  1):读模式
     *      缓存穿透：查询一个null数据。解决：缓存空数据；cache-null-values=true;
     *      缓存击穿:大量并发进来同时查询一个正好过期的数据。解决：加锁：？ 默认没有加锁 @Cacheable 设置sync=true(解决击穿)
     *      缓存雪崩：大量的key同时过期。解决：加上随机时间。加上过期时间：spring.cache.redis.time-to-live=3600000
     *  2)：写模式（缓存与数据库保持一致性）
     *      1）：读写加锁
     *      2）：引入Canal,感知到MySQL的更新去更新缓存
     *      3）:读多写多，直接去数据库查询。
     *
     *
     * @return
     */
    @Cacheable(value = {"category"}, key = "#root.method.name")
    @Override
    public List<CategoryEntity> getFirstCategories() {
        log.info("getFirstCategories.....");
        // 计算索引耗费时间
        long startTime = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = this.baseMapper
                .selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        log.info("总耗费时间:{}", System.currentTimeMillis() - startTime);
        return categoryEntities;
    }

    /**
     * 使用缓存
     *
     * @return
     */
    @Cacheable(value = "category", key = "#root.methodName")
    @Override
    public Map<String, List<Catalog2Vo>> getCatalogJson() {
        /**
         * 1、将数据库中的查询变为一次  优化业务逻辑  避免频繁查询数据库
         */
        // 查出所有分类
        List<CategoryEntity> allCategories = baseMapper.selectList(null);
        // 先查出所有的一级分类
        List<CategoryEntity> firstCategories = getParentCid(allCategories, 0L);
        // 根据一级分类 id组装结构
        Map<String, List<Catalog2Vo>> map = Optional.ofNullable(firstCategories).orElse(Collections.emptyList()).stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // 根据一级分类id找到二级分类数据
            List<CategoryEntity> level2 = getParentCid(allCategories, v.getCatId());
            List<Catalog2Vo> catalog2Vos = null;

            if (!CollectionUtils.isEmpty(level2)) {
                catalog2Vos = level2.stream().map(l2 -> {
                    Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    // 根据二级分类id找到三级分类数据
                    List<CategoryEntity> level3 = getParentCid(allCategories, l2.getCatId());
                    if (!CollectionUtils.isEmpty(level3)) {
                        List<Catalog2Vo.Catalog3Vo> catalog3VoList = level3.stream().map(l3 -> {
                            Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                        catalog2Vo.setCatalog3List(catalog3VoList);
                    }
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        return map;
    }

    /**
     * TODO 产生堆外内存溢出 OutOfDirectMemoryError
     *  1、springboot2.0以后默认使用lettuce作为操作redis的客户端 使用netty进行网络通信
     *  2、lettuce的bug导致netty堆外内存溢出
     *  解决方案：
     *  1、升级lettuce客户端 2、切换使用jedis客户端
     *
     * @return
     */

    public Map<String, List<Catalog2Vo>> getCatalogJson2() {
        // 给缓存中加入json字符串 拿出的json字符串 还需要逆转为能用的对象类型 虚拟化与反序列化
        /**
         * 1、空结果缓存 ：解决缓存穿透问题
         * 2、设置过期时间 (加随机值)：解决缓存雪崩
         * 3、加锁 ：解决缓存击穿
         */
        // 加入缓存逻辑 缓存中存的是json字符串
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        // 缓存中的数据不为空时
        if (StringUtils.hasText(catalogJSON)) {
            log.info("缓存命中....直接返回数据.....");
            Map<String, List<Catalog2Vo>> resultMap = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
            });
            return resultMap;
        }
        log.info("缓存不命中....将要查询数据库.....");
        // 当缓存数据为空时  查询数据库
        Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
        return catalogJsonFromDb;
    }

    /**
     * 使用redisson
     * 缓存里的数据如何和数据库的数据保持一致？？
     * 缓存数据一致性
     * 1)、双写模式
     * 2)、失效模式
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        //  锁的粒度越细越好 越细越快   具体缓存的是某个数据，11号商品） product-11-lock
        // 创建读写锁 避免数据不一致
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
        RLock rLock = readWriteLock.readLock();
        Map<String, List<Catalog2Vo>> dataFromDb = null;
        try {
            rLock.lock();
            // 加锁成功 执行下面业务... 阻塞等待
            dataFromDb = getDataFromDb();
        } finally {
            rLock.unlock();
        }
        return dataFromDb;
    }

    /**
     * 使用redis 作为分布式锁
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        // 1、占分布式锁 redis占坑 此时占锁和设置过期时间必须是原子性
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            log.info("获取分布式锁成功.....");
            // 加锁成功 执行业务
            // 2、设置过期时间必须和加锁是同步的 原子的
            //  redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
            // 无论业务执行过程中是否出现问题 最后都要执行删除锁
            Map<String, List<Catalog2Vo>> dataFromDb = null;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                // 执行lua 脚本并删除锁  传入key 和token  脚本返回的是Long类型的数据 是否成功
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            // 获取值+对比成功删除=原子操作  使用lua脚本解锁 获取锁的时候会有网络延时
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(lockValue)) {
//                // 删除自己的锁
//                redisTemplate.delete("lock");
//            }
            return dataFromDb;
        } else {
            // 加锁失败 加入重试机制
            // 休眠100ms重试
            log.info("获取分布式锁失败....等待重试");
            try {
                TimeUnit.MICROSECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonFromDbWithRedisLock();
        }

    }

    /**
     * 调用业务逻辑 从数据库中查询数据
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithLocalLock() {
//        Map<String, List<Catalog2Vo>> catalogJson = (Map<String, List<Catalog2Vo>>) cache.get("catalogJson");
//        if (catalogJson == null) {
//            // 调用业务逻辑
//            // 将数据放到本地缓存map中 本地缓存容易导致几台服务数据不一致
//            cache.put("catalogJson", "xxx");
//        }
        // 只要是同一把锁 就能锁住这个嗦的所有线程
        // 1、synchronized (this) ：SpringBoot 所有的组件在容器中都是单例的
        // TODO 本地锁：synchronized JUC(Lock) 在分布式情况下 想要锁住所有 必须使用分布式锁
        synchronized (this) {
            return getDataFromDb();
        }
    }

    private Map<String, List<Catalog2Vo>> getDataFromDb() {
        // 得到锁以后 先判断缓存中是否有数据
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        // 缓存中的数据不为空时
        if (StringUtils.hasText(catalogJSON)) {
            Map<String, List<Catalog2Vo>> resultMap = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
            });
            return resultMap;
        }
        log.info("查询了数据库.......");
        /**
         * 1、将数据库中的查询变为一次  优化业务逻辑  避免频繁查询数据库
         */
        // 查出所有分类
        List<CategoryEntity> allCategories = baseMapper.selectList(null);
        // 先查出所有的一级分类
        List<CategoryEntity> firstCategories = getParentCid(allCategories, 0L);
        // 根据一级分类 id组装结构
        Map<String, List<Catalog2Vo>> map = Optional.ofNullable(firstCategories).orElse(Collections.emptyList()).stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // 根据一级分类id找到二级分类数据
            List<CategoryEntity> level2 = getParentCid(allCategories, v.getCatId());
            List<Catalog2Vo> catalog2Vos = null;

            if (!CollectionUtils.isEmpty(level2)) {
                catalog2Vos = level2.stream().map(l2 -> {
                    Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    // 根据二级分类id找到三级分类数据
                    List<CategoryEntity> level3 = getParentCid(allCategories, l2.getCatId());
                    if (!CollectionUtils.isEmpty(level3)) {
                        List<Catalog2Vo.Catalog3Vo> catalog3VoList = level3.stream().map(l3 -> {
                            Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                        catalog2Vo.setCatalog3List(catalog3VoList);
                    }
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        // 将数据放入缓存的步骤要放到同步代码块中 避免第一个线程释放锁之后还没来得及存放数据 下一个线程就进入代码块
        String jsonString = JSON.toJSONString(map);
        // 将查询到的数据放入缓存中
        redisTemplate.opsForValue().set("catalogJSON", jsonString);
        return map;
    }

    /**
     * @param allCategories 所有分类
     * @param catId         分类Id
     * @return
     */
    private List<CategoryEntity> getParentCid(List<CategoryEntity> allCategories, Long catId) {
        List<CategoryEntity> categoryEntities = allCategories.stream().filter(item -> Objects.equals(catId, item.getParentCid())).collect(Collectors.toList());
        return categoryEntities;
    }

    /**
     * 递归 查询完整路径
     *
     * @param catelogId
     * @param path
     * @return
     */
    private List<Long> findParentPath(Long catelogId, List<Long> path) {
        // 收集当前节点id
        path.add(catelogId);
        // 查询出当前分类
        CategoryEntity byId = this.getById(catelogId);
        // 判断是否有上级分类
        if (byId.getParentCid() != 0) {
            // 当前节点的父id 作为当前节点查询
            findParentPath(byId.getParentCid(), path);
        }
        return path;
    }
}